15|粒子

loading

15.1 不受光的粒子

粒子系统可以使用任何材质,因此我们的渲染管线已经可以渲染它们,但还是存在一些局限性。本节我们将制作不受光的材质用于粒子系统,受光的粒子工作方式相同,只不过需要更多的着色器的属性和光照计算。下面我们制作一个测试场景,其中有一些物体和一个点光源,一个平行光,没有开启后处理。

loading

15.1.1 创建粒子

通过右键GameObject->Effects->Particle System可以创建一个粒子系统,然后我们创建一个材质,使用Unlit.shader,作为粒子系统的默认材质,现在粒子显示的是白色正方形。

loading

1. 现在我们创建一个用于粒子的不受光着色器,复制Unlit.shader并命名为Unlit-Particle.shader,因为粒子是动态的,所以我们不需要Meta Pass,将其删掉。

Shader "CustomRP/Particles/Unlit"

2. 我们使用下面的纹理作为粒子材质的纹理,效果如下:

loading

loading

15.1.2 顶点色

loading

每个粒子都可以使用不同的颜色,我们调整粒子系统组件的Start Color属性,使用Random Between Two Colors模式,并将两种颜色设置为黑和白,这可以让粒子颜色在黑白之间随机,如上图所示。但是现在是看不到效果的,因为我们的粒子Shader还不支持顶点色。

1. 我们想要使每个粒子可以使用不同的颜色,需要在着色器中添加对顶点色的支持,在UnlitPass.hlsl的顶点输入和片元输入结构体中声明顶点颜色属性,在顶点函数中进行颜色值的传递,并通过_VERTEX_COLORS关键字来启用或禁用顶点色的支持。

struct Attributes 
{
float4 color : COLOR;
...
};
struct Varyings
{
#if defined(_VERTEX_COLORS)
float4 color : VAR_COLOR;
#endif
...
};
Varyings UnlitPassVertex(Attributes input)
{
...
#if defined(_VERTEX_COLORS)
output.color = input.color;
#endif
//计算缩放和偏移后的UV坐标
output.baseUV = TransformBaseUV(input.baseUV);
return output;
}

2. 在UnlitInput的InputConfig结构体中声明顶点色属性,默认值为不透明的白色,并在GetBase方法中参与基础颜色的计算。

struct InputConfig 
{
...
float4 color;
};
InputConfig GetInputConfig(float2 baseUV, float2 detailUV = 0.0)
{
...
c.color = 1.0;
return c;
}

float4 GetBase(InputConfig c)
{

return map * color* c.color;
}

3. 在片元函数中进行判断,若启用了顶点色,对InputConfig结构体中声明的顶点色属性进行赋值。

    InputConfig config = GetInputConfig(input.baseUV);
#if defined(_VERTEX_COLORS)
config.color = input.color;
#endif

4. 最后在着色器的属性栏中添加一个控制顶点色启用的切换开关,并声明一个相关的关键字。

[Toggle(_VERTEX_COLORS)] _VertexColors (“Vertex Colors”, Float) = 0

#pragma shader_feature _VERTEX_COLORS

loading

对材质启用顶点色,就可以让粒子使用随机颜色了。但有一点需要注意:当所有粒子的颜色相同时,它们的绘制顺序不重要,但是如果颜色不同,就需要按距离对它们进行排序以获得正确的结果,如下图所示。还需注意,当基于距离对粒子进行排序时,可能会突然交换粒子的绘制顺序,因为视图的位置发生了变化,就像任何透明对象一样。

loading

15.1.3 Flipbooks

通过循环不同的基础纹理可以对Billboard粒子实现动画播放,Unity将其称为Flipbooks粒子,这通常使用常规网格布置的纹理图集完成,类似下图这种包含循环噪声模式的4×4网格纹理一样。

loading

我们新建一个材质使用该纹理,让粒子系统使用该材质。现在每个粒子都代表一朵云,我们将粒子的Start Size设置大一些,然后启用Texture Sheet Animation,将Tiles设置为4×4,保证跟我们的网格纹理一样。Start Frame可以设置一个随机开始帧,因为我们是4×4,所以设置为0-15的随机帧,然后Time Mode使用默认的一个粒子的生命周期。

loading

15.1.4 Flipbook混合

当粒子系统处于活跃状态时,粒子会循环几帧,因为Flipbook帧率很低,对于生命周期为5秒的粒子来说其每秒只有3.2帧。这可以通过在连续帧之间混合来平滑过渡,这需要我们在着色器中拿到第二组UV坐标和动画混合因子,我们通过在粒子系统的Renderer模块中启用Custom Vertex Streams来实现。它的作用是声明在材质的顶点着色器中配置哪些粒子的属性可用,我们添加UV2和AnimBlend。可以删除我们不使用的Normal Stream。

loading

添加完自定义顶点流之后会出现一个错误提示,它指示粒子系统和当前使用的着色器不匹配,我们通过在着色器中使用这些顶点流来解决问题。

1. 首先在着色器的属性栏中添加一个切换开关来控制是否使用Flipbook混合,然后声明一个其关联的关键字。

[Toggle(_FLIPBOOK_BLENDING)] _FlipbookBlending (“Flipbook Blending”, Float) = 0

#pragma shader_feature _FLIPBOOK_BLENDING

2. 如果启用了Flipbook混合,在顶点输入结构体中声明2套UV,我们可以将它们合并存储在一个float4类型的向量中,随后声明一个混合因子。

struct Attributes 
{

#if defined(_FLIPBOOK_BLENDING)
float4 baseUV : TEXCOORD0;
float flipbookBlend : TEXCOORD1;
#else
float2 baseUV : TEXCOORD0;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};

3. 在片元输入结构体中声明一个float3类型的向量,用于在顶点函数中存储UV和混合因子。

struct Varyings 
{

#if defined(_FLIPBOOK_BLENDING)
float3 flipbookUVB : VAR_FLIPBOOK;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings UnlitPassVertex(Attributes input)
{

output.baseUV.xy = TransformBaseUV(input.baseUV.xy);
#if defined(_FLIPBOOK_BLENDING)
output.flipbookUVB.xy = TransformBaseUV(input.baseUV.zw);
output.flipbookUVB.z = input.flipbookBlend;
#endif
return output;
}

4. 在InputConfig结构体中声明这个flipbookUVB向量和一个用于判断是否启用了Flipbook混合的bool值,默认为false。

struct InputConfig 
{

float3 flipbookUVB;
bool flipbookBlending;
};
InputConfig GetInputConfig(float2 baseUV, float2 detailUV = 0.0) {

c.flipbookUVB = 0.0;
c.flipbookBlending = false;
return c;
}

5. 如果启用了Flipbook混合,则使用Flipbook的UV坐标对基础纹理进行二次采样,然后和第一次采样的数据根据混合因子进行插值混合。

float4 GetBase(InputConfig c) 
{
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.baseUV);
if (c.flipbookBlending)
{
baseMap = lerp(baseMap, SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, c.flipbookUVB.xy),c.flipbookUVB.z);
}
float4 baseColor = INPUT_PROP(_BaseColor);
return baseMap * baseColor* c.color;
}

6. 最后在片元函数中根据启用情况给这俩声明的属性赋值。

#if defined(_VERTEX_COLORS)
config.color = input.color;
#endif
#if defined(_FLIPBOOK_BLENDING)
config.flipbookUVB = input.flipbookUVB;
config.flipbookBlending = true;
#endif

loading


15.2 相机附近粒子淡化处理

当相机在粒子系统内时,部分粒子会非常靠近相机的近平面,并且会从一侧穿到另一侧。粒子系统组件有一个Render->Max Particle Size的属性,可以防止单个Billboard粒子过大,一旦它们达到最大可见大小时,它们会滑开,而不是接近相机的近平面时越来越大。处理靠近相机近平面粒子的另一个方法是根据它们的片元深度将其淡出。

15.2.1 片元数据

我们已经在片元函数之外有了可用的片元深度,它是使用SV_POSITION语义定义的float4类型的数据,我们曾在LitPass使用了它的XY分量计算抖动值,现在我们正式使用该片元数据。

1. 在顶点函数中,SV_POSITION表示顶点在裁剪空间中的位置,为4D齐次坐标,但在片元函数中SV_POSITION表示片元在屏幕空间中的位置,空间转换由GPU执行。我们将Varyings结构体中声明的postionCS属性重命名为positionCS_SS,且修改在顶点函数中赋值时的命名。

float4 positionCS_SS : SV_POSITION;

2. 下面我们添加一个Fragment.hlsl文件,目前包含一个Fragment结构体和一个GetFragment方法,在得到屏幕空间的位置情况下返回该片元结构数据。目前结构体中只有一个float2的矢量,它存储屏幕空间位置的XY分量,这些是带有0.5偏移的纹素坐标,即屏幕左下角纹素为(0.5,0.5),屏幕右侧为(1.5,0.5)。

#ifndef FRAGMENT_INCLUDED
#define FRAGMENT_INCLUDED
struct Fragment
{
float2 positionSS;
};
Fragment GetFragment (float4 positionSS)
{
Fragment f;
f.positionSS = positionSS.xy;
return f;
}
#endif

3. 在Common文件中所有其它文件include之后将Fragment文件include进来,调整ClipLOD方法,第一个传参由Fragment结构体代替。

#include “Packages/com.unity.render-pipelines.core/ShaderLibrary/Packing.hlsl”
#include “Fragment.hlsl”
void ClipLOD (Fragment fragment, float fade)
{
#if defined(LOD_FADE_CROSSFADE)
float dither = InterleavedGradientNoise(fragment.positionSS, 0);
clip(fade + (fade < 0.0 ? dither : -dither));
#endif
}

4. 在include Fragment文件之前还需声明一个线性和一个点clamp采样器状态,后面我们会在多处使用它们。

SAMPLER(sampler_linear_clamp);
SAMPLER(sampler_point_clamp);
#include “Fragment.hlsl”

5. 然后从PostFXStackPasses.hlsl中删除线性采样器的声明,防止重复声明。

TEXTURE2D(_PostFXSource2);
TEXTURE2D(_ColorGradingLUT);
//SAMPLER(sampler_linear_clamp);

6. 在LitInput和UnlitInput文件的InputConfig结构体中声明Fragment结构数据,并将屏幕空间的位置作为GetInputConfig方法的第一个传参添加进来。

struct InputConfig 
{
Fragment fragment;

};
InputConfig GetInputConfig(float4 positionSS,float2 baseUV, float2 detailUV = 0.0)
{
InputConfig c;
c.fragment = GetFragment(positionSS);

}

7. 在两个片元函数中调用GetInputConfig方法时添加传参。

InputConfig config = GetInputConfig(input.positionCS_SS,input.baseUV);

8. 在LitPassFragment中还需要调整ClipLOD方法的调用时机,放在获取输入配置后调用,将片元结构数据传递给它。另外还要将片元数据的屏幕空间位置传递给InterleavedGradientNoise方法的调用。

float4 LitPassFragment(Varyings input) : SV_TARGET 
{
UNITY_SETUP_INSTANCE_ID(input);
//ClipLOD(input.positionCS.xy, unity_LODFade.x);
InputConfig config = GetInputConfig(input.positionCS_SS,input.baseUV);
ClipLOD(config.fragment, unity_LODFade.x);

//计算抖动值
surface.dither = InterleavedGradientNoise(config.fragment.positionSS, 0);

}

9. ShadowCasterPassFragment方法的ClipLOD方法调用时机也要修改。

void ShadowCasterPassFragment(Varyings input) 
{
UNITY_SETUP_INSTANCE_ID(input);
//ClipLOD(input.positionCS.xy, unity_LODFade.x);
InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);
ClipLOD(config.fragment, unity_LODFade.x);

}

15.2.2 片元深度

1. 要使靠近相机的粒子淡出,我们需要知道片元的深度值,在Fragment结构体中声明片元深度属性。片元深度存储在屏幕空间位置矢量的W分量中,这种透视除法是将3D位置投射到2D屏幕的值。这是视图空间的深度,所以它是距离相机的XY平面而不是近平面的值。

struct Fragment 
{
float2 positionSS;
float depth;
};
Fragment GetFragment (float4 positionSS)
{

f.depth = positionSS.w;
return f;
}

2. 我们可以在LitPassFragment和UnlitPassFragment这两个片元函数中直接返回片元深度(缩小20倍))来检查是否正确,通常将其视为灰度梯度。

    InputConfig config = GetInputConfig(input.positionCS_SS,input.baseUV);
return float4(config.fragment.depth.xxx / 20.0, 1.0);

loading

15.2.3 正交深度

1. 上面的方法只有在使用透视相机时有效,正交相机没有透视除法,因此屏幕空间位置矢量W分量始终为1。我们可以通过在UnityInput文件的UnityPerDraw缓冲区中声明unity_OrthoParams字段来确定我们正在处理的是否是正交相机,Unity会通过这个字段将正交相机的数据信息传递给GPU。

float4 _ProjectionParams;
//正交相机信息
float4 unity_OrthoParams;

2. 若是正交相机,屏幕空间位置W分量始终为1,否则为0。我们可以在Common文件include Fragment文件之前定义一个IsOrthographicCamera方法判断是否使用的是正交相机,可以直接硬编码返回false,或者通过关键字控制返回值。

//根据unity_OrthoParams的W分量是0还是1判断是否使用的是正交相机
bool IsOrthographicCamera ()
{
return unity_OrthoParams.w;
}

对于正交相机我们需要依靠屏幕空间位置的Z分量,该分量包含片元转换后的裁剪空间深度,这是用于深度比较的原始值。如果开启了深度写入,则将其写入深度缓冲区。它是[0,1]范围内的值,对正交投影来说该值是线性的,若要将其转换为视图空间的深度,我们要进行缩放到相机的近-远距离,然后添加近平面距离。近距离和远距离存储在_ProjectionParams的Y和Z分量中。如果使用了反向深度缓冲区,我们还需要反转原始深度。

3. 我们在Common文件include Fragment文件之前定义一个OrthographicDepthBufferToLinear方法来进行该操作。

float OrthographicDepthBufferToLinear (float rawDepth) 
{
#if UNITY_REVERSED_Z
rawDepth = 1.0 - rawDepth;
#endif
return (_ProjectionParams.z - _ProjectionParams.y) * rawDepth + _ProjectionParams.y;
}

4. 在GetFragment方法中调用IsOrthographicCamera方法检查是否使用了正交相机,如果使用了就调用OrthographicDepthBufferToLinear方法得到正确的片元深度。

Fragment GetFragment (float4 positionSS) 
{

f.depth = IsOrthographicCamera() ? OrthographicDepthBufferToLinear(positionSS.z) : positionSS.w;
return f;
}

下图是正交相机的片元深度。

loading

在验证了两种相机类型的片元深度拿到的是正确的之后,就可以将LitPassFragment和UnlitPassFragment方法中的返回深度的测试代码删除了。

15.2.4 基于距离的淡化

1. 在着色器的属性栏中添加一个控制基于距离淡化启用的切换开关,然后再添加两个可以调整淡化距离和范围的属性。距离决定粒子完全消失在离相机平面多近的地方,这里指的是相机的近平面,而不是其附近的位置,通常设置默认值为1,超过范围区域的粒子将线性地淡出。

        [Toggle(_NEAR_FADE)] _NearFade (“Near Fade”, Float) = 0
_NearFadeDistance (“Near Fade Distance”, Range(0.0, 10.0)) = 1
_NearFadeRange (“Near Fade Range”, Range(0.01, 10.0)) = 1

2. 然后声明一个_NEAR_FADE关键字。

#pragma shader_feature _NEAR_FADE

3. 在UnlitInput文件的UnityPerMaterial缓冲区中声明淡化距离和范围属性。

UNITY_DEFINE_INSTANCED_PROP(float, _NearFadeDistance)
UNITY_DEFINE_INSTANCED_PROP(float, _NearFadeRange)

4. 在InputConfig结构体中添加一个相关bool类型的字段,默认为false。

struct InputConfig 
{

bool nearFade;
};
InputConfig GetInputConfig(float4 positionSS,float2 baseUV, float2 detailUV = 0.0)
{

c.nearFade = false;
return c;
}

5. 在GetBase方法中进行基于距离的淡化处理,通过降低表面片元的Alpha值,可以使粒子在靠近相机近平面时淡化处理。衰减因子是等于片元深度减去淡化距离的差,再除以淡化范围得到。最后使用saturate方法将衰减结果控制在[0,1]之间,保证为正数。

float4 GetBase(InputConfig c) 
{

if (c.nearFade) {
float nearAttenuation = (c.fragment.depth - INPUT_PROP(_NearFadeDistance)) / INPUT_PROP(_NearFadeRange);
baseMap.a = saturate(nearAttenuation);
}
float4 baseColor = INPUT_PROP(_BaseColor);
return baseMap * baseColor
c.color;
}

6. 最后在UnlitPassFragment方法中根据关键字控制粒子淡化功能的启用。

#if defined(_FLIPBOOK_BLENDING)
config.flipbookUVB = input.flipbookUVB;
config.flipbookBlending = true;
#endif
#if defined(_NEAR_FADE)
config.nearFade = true;
#endif

loading


15.3 软粒子

当Billboard粒子和几何体相交时,锐利的过渡在视觉上不太和谐,又使其平滑性质显而易见。解决这个问题的方案是使用软粒子(Soft Particle),当软粒子后面有不透明的几何形状时软粒子就会消失。这需要将粒子的片元深度和之前绘制到相机缓冲区相同位置的其它物体的深度作比较,这意味着我们要对深度缓冲区进行采样。

15.3.1 分离深度缓冲

目前我们一直为相机使用一个帧缓冲区,其中包含颜色和深度信息,这是典型的帧缓冲配置。我们把颜色和深度数据始终存储在单独的缓冲区中,称为缓冲区附件。要访问深度缓冲区我们需要单独定义这些附件。

1. 首先我们将CameraRenderer脚本中的_CameraFrameBuffer着色器标识ID换成颜色和深度附件的着色器标识ID。

    //static int frameBufferId = Shader.PropertyToID("_CameraFrameBuffer");
static int colorAttachmentId = Shader.PropertyToID("_CameraColorAttachment");
static int depthAttachmentId = Shader.PropertyToID("_CameraDepthAttachment");

2. 在Render方法中我们将颜色附件传递到PostFXStack的Render方法中。

       if (postFXStack.IsActive)
{
postFXStack.Render(colorAttachmentId);
}

3. 在Setup方法中,我们要得到这两个单独的缓冲区,但颜色缓冲区没有深度信息,而深度缓冲区的格式是RenderTextureFormat.Depth,滤波模式为FilterMode.Point,因为混合深度数据没有意义。可以通过一次SetRenderTarget方法的调用设置这两个缓冲区附件,为每个附件使用相同的加载和存储操作。

    void Setup()
{

if (postFXStack.IsActive)
{
if (flags > CameraClearFlags.Color)
{
flags = CameraClearFlags.Color;
}
buffer.GetTemporaryRT(colorAttachmentId, camera.pixelWidth, camera.pixelHeight,0, FilterMode.Bilinear,
useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
buffer.GetTemporaryRT(depthAttachmentId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);
buffer.SetRenderTarget(colorAttachmentId,RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store,
depthAttachmentId,RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
}


}

4. 最后在Cleanup方法中释放这两个缓冲区,我们的渲染管线现在还可以像以前一样正常工作,不过现在我们可以单独地访问颜色缓冲和深度缓冲。

  void Cleanup()
{
lighting.Cleanup();
if (postFXStack.IsActive)
{
buffer.ReleaseTemporaryRT(colorAttachmentId);
buffer.ReleaseTemporaryRT(depthAttachmentId);
}
}

15.3.2 拷贝深度

1. 我们不能在深度缓冲区用于渲染的同时进行采样,我们必须复制它。声明一个_CameraDepthTexture着色器标识ID,并定义一个bool字段用于判断是否在使用深度纹理,只有在需要时才考虑拷贝深度。在Render方法中我们在获取相机的设置后确定是否在使用深度纹理,目前我们始终启用它。

    static int depthTextureId = Shader.PropertyToID("_CameraDepthTexture");
//是否正在使用深度纹理
bool useDepthTexture;
public void Render()
{

CameraSettings cameraSettings = crpCamera ? crpCamera.Settings : defaultCameraSettings;
useDepthTexture = true;

}

2. 定义一个CopyAttachments方法用在useDepthTexture为true时获得一个临时重复深度纹理,并调用buffer.CopyTexture方法将深度附件的数据拷贝到临时深度纹理中。在Cleanup方法中释放这个临时深度纹理。

    void Cleanup()
{

//释放临时深度纹理
if (useDepthTexture)
{
buffer.ReleaseTemporaryRT(depthTextureId);
}
}
//拷贝深度数据
void CopyAttachments()
{
if (useDepthTexture)
{
buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);
buffer.CopyTexture(depthAttachmentId, depthTextureId);
ExecuteBuffer();
}
}

3. 在Render方法中,我们在绘制天空盒之后进行深度拷贝,这意味着深度纹理仅在渲染透明物体时可用。

        context.DrawSkybox(camera);
CopyAttachments();

15.3.3 没有后处理的拷贝深度

1. 拷贝深度的前提是存在深度附件,而目前只有在启用后处理的情况下才有深度附件。为了在没有后处理的情况下也能使用,我们需要在使用深度纹理时使用中间帧缓冲区。定义一个bool变量来进行跟踪。在Setup方法中获得附件之前进行判断,若使用了深度纹理或启用了后处理,该bool值应为true。同样Cleanup方法的判断条件也进行调整。

    //是否使用中间帧缓冲
bool useIntermediateBuffer;
void Setup()
{

useIntermediateBuffer = useDepthTexture || postFXStack.IsActive;
if (useIntermediateBuffer)
{
if (flags > CameraClearFlags.Color)
{
flags = CameraClearFlags.Color;
}

}


}
void Cleanup()
{

if (useIntermediateBuffer)
{
//释放颜色和深度纹理
buffer.ReleaseTemporaryRT(colorAttachmentId);
buffer.ReleaseTemporaryRT(depthAttachmentId);
//释放临时深度纹理
if (useDepthTexture)
{
buffer.ReleaseTemporaryRT(depthTextureId);
}
}
}

2. 但现在没有启用后处理渲染会失败,因为我们渲染到了中间帧缓冲区里面。我们还需要最后拷贝到相机目标中,但Copy Texture只能复制到渲染纹理,不能复制到最终帧缓冲区。我们可以使用后处理的Copy Pass来进行复制,但这个Pass特定于相机渲染。创建一个相机渲染专用的Shader,将后处理Shader的Copy Pass代码块拷贝过来。

Shader “Hidden/CustomRP/Camera Renderer” 
{
SubShader
{
Cull Off
ZTest Always
ZWrite Off
HLSLINCLUDE
#include “…/ShaderLibrary/Common.hlsl”
#include “CameraRendererPasses.hlsl”
ENDHLSL
Pass
{
Name “Copy”
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment CopyPassFragment
ENDHLSL
}
}
}

3. 我们复制PostFXStackPasses.hlsl文件,重新命名为CameraRendererPasses.hlsl。保留Varyings结构体和DefaultPassVertex方法,其余全部删掉,并声明一个源纹理和CopyPassFragment方法,起初只返回对源纹理的采样结果。

#ifndef CUSTOM_CAMERA_RENDERER_PASSES_INCLUDED
#define CUSTOM_CAMERA_RENDERER_PASSES_INCLUDED
TEXTURE2D(_SourceTexture);
struct Varyings
{

};
Varyings DefaultPassVertex (uint vertexID : SV_VertexID)
{

}
float4 CopyPassFragment (Varyings input) : SV_TARGET
{
return SAMPLE_TEXTURE2D_LOD(_SourceTexture, sampler_linear_clamp, input.screenUV, 0);
}
#endif

4. 在CameraRenderer脚本中添加一个材质字段,并定义一个带有Shader参数的构造方法,方法内会调用CoreUtils.CreateEngineMaterial方法创建一个使用该Shader的材质。同时也定义一个Dispose方法来定期销毁该材质,因为每当渲染管线资源被修改时都会创建一个新的渲染管线实例,这会导致编辑器中会创建许多材质。

    Material material;
public CameraRenderer(Shader shader)
{
material = CoreUtils.CreateEngineMaterial(shader);
}
public void Dispose () {
CoreUtils.Destroy(material);
}

5. 接下来调整CustomRenderPipeline脚本,在渲染管线的构造方法中添加一个Shader传参,并在函数末尾创建CameraRenderer对象。

    //CameraRenderer renderer = new CameraRenderer();
CameraRenderer renderer;
public CustomRenderPipeline(…,Shader cameraRendererShader)
{

renderer = new CameraRenderer(cameraRendererShader);
}

6. 调整CustomRenderPipeline.Editor.cs的Dispose方法,因为它仅用于编辑器中,我们将其重命名为DisposeForEditor,并将其定义为一个局部方法。我们不在这里调用渲染管线的Dispose方法。

   partial void DisposeForEditor();
#if UNITY_EDITOR

partial void DisposeForEditor()
{
//base.Dispose(disposing);
Lightmapping.ResetDelegate();
}
#endif

7. 定义一个新的Dispose方法,它不止适用于编辑器。我们在这里调用CameraRenderer的Dispose方法。

    protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
DisposeForEditor();
renderer.Dispose();
}

8. 在CustomRenderPipelineAsset脚本上方定义一个Shader属性,并将其传递到渲染管线的构造方法中。

    [SerializeField]
Shader cameraRendererShader = default;
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(…, cameraRendererShader);
}

loading

9. 在CameraRenderer脚本中还需定义一个源纹理的着色器标识ID和一个Draw方法,它和PostFXStack的Draw方法类似,只不过我们目前只有一个Pass。

    static int sourceTextureId = Shader.PropertyToID("_SourceTexture");
void Draw(RenderTargetIdentifier from, RenderTargetIdentifier to)
{
buffer.SetGlobalTexture(sourceTextureId, from);
buffer.SetRenderTarget(to, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
buffer.DrawProcedural(Matrix4x4.identity, material, 0, MeshTopology.Triangles, 3);
}

10. 在Render方法中进行判断,如果没有启用后处理但使用了中间帧缓冲区,则调用Draw方法将颜色附件数据拷贝到相机目标中。

        if (postFXStack.IsActive)
{
postFXStack.Render(colorAttachmentId);
}
else if (useIntermediateBuffer)
{
Draw(colorAttachmentId, BuiltinRenderTextureType.CameraTarget);
ExecuteBuffer();
}

15.3.4 重建观察空间深度

1. 为了对深度纹理进行采样,我们需要屏幕空间的片元UV坐标,可以通过屏幕空间的位置除以屏幕像素尺寸得到它。在UnityInput的UnityPerDraw缓冲区中声明一个_ScreenParams属性,它的XY分量存储的就是屏幕像素尺寸。

float4 _ScreenParams;

2. 在Fragment.hlsl中声明相机的深度纹理,并在Fragment结构体中定义屏幕空间的UV和缓冲区深度属性,通过SAMPLE_DEPTH_TEXTURE宏对相机深度纹理进行采样来获取缓冲区深度。该宏和SAMPLE_TEXTURE2D类似,只不过它仅返回R通道的值。

TEXTURE2D(_CameraDepthTexture);
struct Fragment
{

//屏幕空间UV坐标
float2 screenUV;
//深度缓冲
float bufferDepth;
};
Fragment GetFragment (float4 positionSS)
{

f.screenUV = f.positionSS / _ScreenParams.xy;
f.depth = IsOrthographicCamera() ? OrthographicDepthBufferToLinear(positionSS.z) : positionSS.w;
f.bufferDepth =SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, f.screenUV, 0);
return f;
}

3. 这为我们提供了原始深度缓冲值,为了将其转换为观察空间的深度,我们可以在使用正交相机的情况下再次调用OrthographicDepthBufferToLinear方法,透视深度也需要转换。我们使用LinearEyeDepth方法,它需要_ZBufferParams作为第二个参数。

Fragment GetFragment (float4 positionSS) 
{

f.bufferDepth =SAMPLE_DEPTH_TEXTURE_LOD(_CameraDepthTexture, sampler_point_clamp, f.screenUV, 0);
f.bufferDepth = IsOrthographicCamera() ? OrthographicDepthBufferToLinear(f.bufferDepth) : LinearEyeDepth(f.bufferDepth, _ZBufferParams);
return f;
}

4. 在UnityInput的UnityPerDraw缓冲区中声明_ZBufferParams属性,它包含了从原始深度到线性深度的转换因子。

float4 _ZBufferParams;

5. 可以像之前测试片元深度一样,在UnlitPassFragment方法中直接返回采样后的缓冲区深度,来检查是否进行了正确的采样。测试完后记得删除测试代码。

InputConfig config = GetInputConfig(input.positionCS_SS, input.baseUV);
return float4(config.fragment.bufferDepth.xxx / 20.0, 1.0);

loading

15.3.5 可选深度纹理

1. 深度拷贝需要做一些额外工作,尤其是没用启用后处理时,因为这需要中间帧缓冲器和额外拷贝到相机目标,因此我们把深度拷贝设置为可选项,我们创建一个CameraBufferSettings.cs脚本,其中定义一个CameraBufferSettings结构体,这个脚本用于与摄像机缓冲区相关的所有设置进行分组。目前包含了是否进行深度拷贝、是否启用HDR和一个开关控制渲染反射时是否拷贝深度。因为反射是在没有启用后处理的情况下渲染的,粒子系统也不会出现在反射中,因为反射的深度拷贝成本很高,而且可能毫无用处。我们这么做也是因为深度也可以用于其它效果,这在反射中可能也是可见的。需要注意的是,每个立方体反射面的深度缓冲是不同的,因此立方体纹理边缘会有深度接缝。

[System.Serializable]
public struct CameraBufferSettings
{
public bool allowHDR;
public bool copyDepth;
public bool copyDepthReflection;
}

2. 我们将CustomRenderPipelineAsset脚本中控制启用HDR的开关替换成一个CameraBufferSettings对象,并在实例化渲染管线时替换传参。

    //[SerializeField]
//bool allowHDR = true;
[SerializeField]
CameraBufferSettings cameraBuffer = new CameraBufferSettings
{
allowHDR = true
};
protected override RenderPipeline CreatePipeline()
{
return new CustomRenderPipeline(cameraBuffer, …);
}

3. 在CustomRenderPipeline脚本中也进行相关修改。

    //bool allowHDR;
CameraBufferSettings cameraBufferSettings;
public CustomRenderPipeline(CameraBufferSettings cameraBufferSettings, …)
{
//this.allowHDR = allowHDR;
this.cameraBufferSettings = cameraBufferSettings;

}
protected override void Render(ScriptableRenderContext context, Camera[] cameras)
{
foreach (Camera camera in cameras)
{
renderer.Render(context, camera, cameraBufferSettings, …);
}
}

4. 对CameraRenderer的Render方法进行调整,根据相机类型来使用对应的设置。

    public void Render(ScriptableRenderContext context, Camera camera, CameraBufferSettings bufferSettings,
)

{

//useDepthTexture = true;
if (camera.cameraType == CameraType.Reflection)
{
useDepthTexture = bufferSettings.copyDepthReflection;
}
else
{
useDepthTexture = bufferSettings.copyDepth;
}

useHDR = bufferSettings.allowHDR && camera.allowHDR;

}

loading

5. 除了对整个渲染管线设置外,我们还可以给CameraSettings也添加个控制深度拷贝的切换开关,默认为启用。

public bool copyDepth = true;

loading

6. 调整CameraRenderer的Render方法,对于常规相机,只有在渲染管线和相机都启用了深度拷贝时才使用深度纹理,这与HDR的控制方式类似。

        if (camera.cameraType == CameraType.Reflection)
{
useDepthTexture = bufferSettings.copyDepthReflection;
}
else
{
useDepthTexture = bufferSettings.copyDepth && cameraSettings.copyDepth;
}

15.3.6 缺失纹理

1. 由于深度纹理是可选的,它可能不存在,那么当在Shader中进行纹理采样时,结果也是随机的。该纹理可能为空,也可能是旧的拷贝,也可能是其它相机。在渲染不透明物体时,Shader也可能过早的对深度纹理进行采样,所以我们需要确保无效的采样也能得到正确结果。我们通过在CameraRender脚本的构造函数中默认创建一个1×1大小的缺失纹理来解决这个问题,将其隐藏标志设置为HideFlags.HideAndDontSave,纹理命名为Missing,可以在通过帧调试器检查着色器属性时很明显地发现是否使用了错误的纹理。同时,在Dispose方法中记得销毁它。

    Texture2D missingTexture;
public CameraRenderer(Shader shader)
{
material = CoreUtils.CreateEngineMaterial(shader);
missingTexture = new Texture2D(1, 1)
{
hideFlags = HideFlags.HideAndDontSave,
name = “Missing”
};
missingTexture.SetPixel(0, 0, Color.white * 0.5f);
missingTexture.Apply(true, true);
}
public void Dispose()
{
CoreUtils.Destroy(material);
CoreUtils.Destroy(missingTexture);
}

2. 在Setup方法的末尾将缺失纹理作为深度纹理。

    void Setup()
{

buffer.BeginSample(SampleName);
buffer.SetGlobalTexture(depthTextureId, missingTexture);
ExecuteBuffer();

}

15.3.7 淡化背景附近的粒子

1. 现在我们有了一个功能深度纹理,可以对软粒子进行操作了。首先向粒子的着色器属性栏中添加一个启用软粒子的切换开关,还要控制软粒子距离和范围的属性,类似于粒子淡化时的操作。距离是从粒子后面的任何东西开始测量的,因此我们默认将其设置为0。

        [Toggle(_SOFT_PARTICLES)] _SoftParticles (“Soft Particles”, Float) = 0
_SoftParticlesDistance (“Soft Particles Distance”, Range(0.0, 10.0)) = 0
_SoftParticlesRange (“Soft Particles Range”, Range(0.01, 10.0)) = 1

2. 然后声明与其相关的关键字。

#pragma shader_feature _SOFT_PARTICLES

3. 在UnlitInput.hlsl的InputConfig结构体中添加一个用于跟踪软粒子启用状态的bool字段,默认设置为false,然后在UnityPerMaterial缓冲区中声明其距离和范围属性。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)

UNITY_DEFINE_INSTANCED_PROP(float, _SoftParticlesDistance)
UNITY_DEFINE_INSTANCED_PROP(float, _SoftParticlesRange)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
struct InputConfig
{

bool softParticles;
};
InputConfig GetInputConfig(float4 positionSS,float2 baseUV, float2 detailUV = 0.0)
{

c.softParticles = false;
return c;
}

4. 在片元函数中根据关键字是否定义来设置该bool值。

#if defined(_NEAR_FADE)
config.nearFade = true;
#endif
#if defined(_SOFT_PARTICLES)
config.softParticles = true;
#endif

5. 在GetBase方法中根据片元的缓冲区深度减去片元自身深度,并应用软粒子距离和范围得到最终的深度衰减,来应用软粒子的淡化功能。

float4 GetBase(InputConfig c) 
{

if (c.softParticles)
{
float depthDelta = c.fragment.bufferDepth - c.fragment.depth;
float nearAttenuation = (depthDelta - INPUT_PROP(_SoftParticlesDistance)) /
INPUT_PROP(_SoftParticlesRange);
baseMap.a = saturate(nearAttenuation);
}
float4 baseColor = INPUT_PROP(_BaseColor);
return baseMap * baseColor
c.color;
}

15.3.8 不拷贝纹理支持

现在一切工作正常,前提是能支持CopyTexture方法直接拷贝纹理。但如果我们想支持WebGL 2.0,只能通过着色器进行拷贝了。尽管这样做效率很低,但至少在WebGL 2.0平台上能够正常工作。

1. 首先在CameraRenderer脚本中定义一个bool字段用来跟踪是否支持纹理拷贝,默认为false。

static bool copyTextureSupported = false;

2. 在CopyAttachments方法中进行判断,如果不支持纹理拷贝,则使用Draw方法。

    void CopyAttachments()
{
if (useDepthTexture)
{
buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);
if (copyTextureSupported)
{
buffer.CopyTexture(depthAttachmentId, depthTextureId);
}
else
{
Draw(depthAttachmentId, depthTextureId);
}
ExecuteBuffer();
}
}

3. 最初无法产生正确结果,因为Draw方法修改了渲染目标,因此进一步绘制会出错。调用Draw方法之后我们要将渲染目标设置回相机缓冲区,并再次加载附件。

           if (copyTextureSupported)
{
buffer.CopyTexture(depthAttachmentId, depthTextureId);
}
else
{
Draw(depthAttachmentId, depthTextureId);
buffer.SetRenderTarget(
colorAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,
depthAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
}

4. 第二个问题是深度不会被拷贝,因为我们的Copy Pass只写入到默认的着色器目标,即颜色数据,而不是深度。要复制深度,我们需要在CameraRenderer.shader中添加第二个拷贝深度的Pass,以写入深度而不是颜色。我们通过将ColorMask设置为0,并开启深度写入来做到这点,它需要一个CopyDepthPassFragment的片元函数,后面再实现它。

Pass {
Name “Copy Depth”
ColorMask 0
ZWrite On
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment CopyDepthPassFragment
ENDHLSL
}

5. 在CameraRendererPasses文件中定义CopyDepthPassFragment方法,对原始深度缓冲区采样,并直接将其用于片元的新深度。

float CopyDepthPassFragment (Varyings input) : SV_DEPTH 
{
return SAMPLE_DEPTH_TEXTURE_LOD(_SourceTexture, sampler_point_clamp, input.screenUV, 0);
}

6. 在CameraRenderer的Draw方法中添加一个bool参数,用于指示是否使用了深度,如果为true则使用第二个Pass。

    void Draw(RenderTargetIdentifier from, RenderTargetIdentifier to, bool isDepth = false)
{

buffer.DrawProcedural(Matrix4x4.identity, material, isDepth ? 1 : 0, MeshTopology.Triangles, 3);
}

7. 然后在复制深度缓冲区的时候指示使用深度。

Draw(depthAttachmentId, depthTextureId, true);

8. 最后,我们通过检查SystemInfo.copyTextureSupport来确定平台是否支持拷贝纹理,只要高于None都是可以的。

//平台是否支持拷贝纹理
static bool copyTextureSupported = SystemInfo.copyTextureSupport > CopyTextureSupport.None;

15.3.9 Gizmos和深度

现在我们有了绘制深度的办法,在结合后处理或使用深度纹理时,可以使得Gizmos能够再次感知深度。在DrawGizmosBeforeFX方法中首先判断是否使用了中间帧缓冲区,如果为true,则将深度数据复制到相机目标。

   partial void DrawGizmosBeforeFX()
{
if (Handles.ShouldRenderGizmos())
{
if (useIntermediateBuffer)
{
Draw(depthAttachmentId, BuiltinRenderTextureType.CameraTarget, true);
ExecuteBuffer();
}
context.DrawGizmos(camera, GizmoSubset.PreImageEffects);
}
}

Gizmos已经能够感知深度。

loading

loading


15.4 扰动(Distortion)效果

我们对Unity粒子支持的另一个特性是扰动,它可以用于由热量引起的大气折射等效果。这需要对颜色缓冲区进行采样,就像我们已经在对深度缓冲区进行采样一样,但需要添加UV偏移。

15.4.1 颜色拷贝纹理

1. 在CameraBufferSettings结构体中添加两个bool类型的切换开关,我们把常规相机和反射相机的颜色拷贝分成两个单独的开关来处理。

    public bool copyColor;
public bool copyColorReflection;

loading

2. 在CameraSettings脚本中也定义一个拷贝颜色的切换开关。

public bool copyColor = true;

3. 在CameraRenderer脚本中声明一个相机颜色纹理的着色器标识ID,并定义一个bool类型的字段用来追踪是否使用了颜色纹理。

static int colorTextureId = Shader.PropertyToID("_CameraColorTexture");
bool useColorTexture;
public void Render ()
{

if (camera.cameraType == CameraType.Reflection)
{
useColorTexture = bufferSettings.copyColorReflection;
useDepthTexture = bufferSettings.copyDepthReflection;
}
else
{
useColorTexture = bufferSettings.copyColor && cameraSettings.copyColor;
useDepthTexture = bufferSettings.copyDepth && cameraSettings.copyDepth;
}

}

4. 对Setup方法进行调整,是否使用中间帧缓冲区还取决于是否使用了颜色纹理,并在方法最后将颜色纹理设置为缺失纹理。同时在Cleanup方法中记得释放。

    void Setup()
{

useIntermediateBuffer = useColorTexture || useDepthTexture || postFXStack.IsActive;

buffer.SetGlobalTexture(colorTextureId, missingTexture);
buffer.SetGlobalTexture(depthTextureId, missingTexture);
ExecuteBuffer();

}
void Cleanup()
{
lighting.Cleanup();
if (useIntermediateBuffer)
{

if (useColorTexture)
{
buffer.ReleaseTemporaryRT(colorTextureId);
}
if (useDepthTexture)
{
buffer.ReleaseTemporaryRT(depthTextureId);
}
}
}

5. 调整DrawVisibleGeometry方法中对CopyAttachments的调用,当至少使用颜色纹理和深度纹理其中一个时才可以拷贝相机附件。

        context.DrawSkybox(camera);
if (useColorTexture || useDepthTexture)
{
CopyAttachments();
}

6. 对CopyAttachments方法也进行调整,让它分别拷贝两个纹理,然后重置渲染目标并执行一次缓冲区。

    void CopyAttachments()
{
if (useColorTexture)
{
buffer.GetTemporaryRT(colorTextureId, camera.pixelWidth, camera.pixelHeight, 0, FilterMode.Bilinear,
useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
if (copyTextureSupported)
{
buffer.CopyTexture(colorAttachmentId, colorTextureId);
}
else
{
Draw(colorAttachmentId, colorTextureId);
}
}
if (useDepthTexture)
{
buffer.GetTemporaryRT(depthTextureId, camera.pixelWidth, camera.pixelHeight,32, FilterMode.Point, RenderTextureFormat.Depth);
if (copyTextureSupported)
{
buffer.CopyTexture(depthAttachmentId, depthTextureId);
}
else
{
Draw(depthAttachmentId, depthTextureId, true);
//buffer.SetRenderTarget(
// colorAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,
// depthAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
}
//ExecuteBuffer();
}
if (!copyTextureSupported)
{
buffer.SetRenderTarget(colorAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store,
depthAttachmentId,RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
}
ExecuteBuffer();
}

15.4.2 采样颜色缓冲

1. 首先在Common.hlsl中我们声明一个用于采样相机颜色纹理的采样器sampler_CameraColorTexture属性。

SAMPLER(sampler_linear_clamp);
SAMPLER(sampler_point_clamp);
SAMPLER(sampler_CameraColorTexture);

2. 在Fragment.hlsl中声明这个颜色纹理,并定义一个GetBufferColor方法采样它。使用片元的屏幕UV坐标加上传入的UV偏移作为最终采样UV。

TEXTURE2D(_CameraColorTexture);
float4 GetBufferColor(Fragment fragment, float2 uvOffset = float2(0.0, 0.0))
{
float2 uv = fragment.screenUV + uvOffset;
return SAMPLE_TEXTURE2D_LOD(_CameraColorTexture, sampler_CameraColorTexture, uv,0);
}

15.4.3 扰动向量

要创建有效的扰动效果,我们需要一张平滑过渡扰动向量的纹理,这是一个单一圆形粒子的简单纹理,同时也作为法线纹理,我们把该纹理导出到工程中。

loading

1. 在粒子着色器属性栏中添加一个控制使用扰动纹理的切换开关,还需添加一个扰动纹理和一个控制扰动强度的属性。扰动强度将作为屏幕空间的UV偏移,因此使用比较小的值。

[Toggle(_DISTORTION)] _Distortion(“Distortion”, Float) = 0
[NoScaleOffset] _DistortionMap(“Distortion Vectors”, 2D) = “bumb” {}
_DistortionStrength(“Distortion Strength”, Range(0.0, 0.2)) = 0.1

loading

2. 同时声明相关的关键字。

#pragma shader_feature _DISTORTION

3. 在UnlitInput文件中声明扰动纹理和采样器,并在UnityPerMaterial缓冲区中声明扰动强度属性。

TEXTURE2D(_DistortionMap);
SAMPLER(sampler_DistortionMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)

UNITY_DEFINE_INSTANCED_PROP(float, _DistortionStrength)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)

4. 定义一个GetDistortion方法,对扰动纹理进行采样,并应用Flipbook混合,最后使用扰动强度解码法线,并返回其XY分量。

float2 GetDistortion(InputConfig c) 
{
float4 rawMap = SAMPLE_TEXTURE2D(_DistortionMap, sampler_DistortionMap, c.baseUV);
if (c.flipbookBlending)
{
rawMap = lerp(rawMap, SAMPLE_TEXTURE2D(_DistortionMap, sampler_DistortionMap, c.flipbookUVB.xy),c.flipbookUVB.z);
}
return DecodeNormal(rawMap, INPUT_PROP(_DistortionStrength)).xy;
}

5. 在UnlitPassFragment方法中进行判断,如果开启了扰动功能,获得扰动数据后将其用作UV偏移获取缓冲颜色,从而替换掉原本的基础颜色,该操作在片元裁剪之后进行。

#if defined(_CLIPPING)
//透明度低于阈值的片元进行舍弃
clip(base.a - GetCutoff(config));
#endif
#if defined(_DISTORTION)
float2 distortion = GetDistortion(config);
base = GetBufferColor(config.fragment, distortion);
#endif

loading

6. 结果是在角落处,粒子会径向扭曲颜色纹理,因为角落处扰动向量为0。但扰动效果应取决于粒子的视觉强度,它应该是由原本的Alpha控制的,因此使用原本Alpha调整扰动偏移矢量。

float2 distortion = GetDistortion(config) * base.a;

7. 现在仍然有硬边,因为粒子完全重叠并且是矩形的,我们通过保留粒子的原始Alpha来隐藏它。

base.rgb = GetBufferColor(config.fragment, distortion).rgb;

loading

15.4.4 扰动混合

1. 当启用扰动效果时,我们完全取代了粒子的原始颜色,只保留了原来的Alpha,可以通过一些方式将粒子颜色和扰动的颜色缓冲区混合,我们在着色器的属性栏中声明一个扰动混合滑块,用于在粒子自身颜色和扰动之间进行插值。

_DistortionStrength(“Distortion Strength”, Range(0.0, 0.2)) = 0.1
_DistortionBlend(“Distortion Blend”, Range(0.0, 1.0)) = 1

2. 在UnlitInput的UnityPerMaterial缓冲区中声明扰动混合属性,并定义一个GetDistortionBlend方法。

UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)

UNITY_DEFINE_INSTANCED_PROP(float, _DistortionBlend)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
float GetDistortionBlend (InputConfig c)
{
return INPUT_PROP(_DistortionBlend);
}

3. 当混合滑块值为1时,我们只会看到扰动,通过降低滑块值使粒子自身的颜色出现,但它不会完全隐藏扰动。我们在片元函数中通过使用粒子的Alpha值减去扰动混合值,来对扰动颜色和粒子颜色之间进行插值。因此当启用扰动时,粒子自身颜色将始终比较淡,并且与关闭扰动效果相比显得更小,除非其完全不透明。

#if defined(_DISTORTION)
float2 distortion = GetDistortion(config) * base.a;
base.rgb = lerp(GetBufferColor(config.fragment, distortion).rgb, base.rgb,saturate(base.a - GetDistortionBlend(config)));
#endif

我们可以对相对复杂的Flipbook粒子使用更丰富的扰动纹理,如下图,这样会达到更好的效果。

loading

loading

源代码及PDF课件地址:

文件下载
暂无评论发表评论